/**
 * Тестирование go.Class
 *
 * go.Class: надстройка над "прототипным" ООП, эмулирующее похожее на классы поведение.
 *
 * @package    go.js
 * @subpackage Class
 * @author     Григорьев Олег aka vasa_c
 */
 
go.Test.module("Class");

/**
 * Создание "класса".
 *
 * Под "классом" подразумевается функция-конструктор, создающая объекты, принадлежащие данному "классу".
 * Класс создаётся с помощью метода go.Class(parent, props).
 * Первым аргументом передаётся родительский класс (см. ниже соотвествующий тест), вторым - набор методов и свойств.
 * В случае, если класс - корневой, вместо parent указывается null. Так же допустим синтаксис go.Class(props).
 *
 * Методы, начинающиеся с префикса "__" и свойства, начинающиеся с "$", go.Class использует под свои нужды.
 */
go.Test("Create and instance class", function() {

    /**
     * Тестовый класс
     *
     * @property string name
     *           имя конкретного объекта
     */
    var TestClass = go.Class({
    
        /**
         * Указать имя данного объекта
         *
         * @param string name
         */
        'setName': (function(name) {
            this.name = name;
        }),
        
        /**
         * Получить имя данного объекта
         */
        'getName': (function() {
            return this.name;
        }),
    
        'eoc': null // End Of Class: просто, чтобы не пропустить висячую запятую
    });
    
    equal(typeof(TestClass), "function", "В качестве класса выступает функция-конструктор");
    
    /* Создание экземпляров класса - как обычно */
    var one = new TestClass();
    var two = new TestClass();
    
    ok(one instanceof TestClass, "Instanceof также работает здесь");
    
    /* Вызов методов "класса" */
    one.setName("One");
    two.setName("Two");
    
    equal(one.getName(), "One", "Объект инкапсулирует в себе данные");
    equal(two.getName(), "Two", "Каждый объект инкапсулирует свои данные");
});

/**
 * В качестве "конструктора" класса выступает метод "__construct()".
 * Конструктор автоматически вызывается при создании объекта.
 */
go.Test("Constructor", function() {

    var TestClass = go.Class({
        
        /**
         * Конструктор
         *
         * @param string name
         *        имя объекта
         */
        '__construct': (function(name) {
            this.name = name;
        }),
        
        'getName': (function() {
            return this.name;
        }),
        
        'eoc': null        
    });
    
    /* В конструктор передаются аргументы, использованные при создании объекта */
    var one = new TestClass("One");

    equal(one.getName(), "One", "Конструктор установил нужные данные");
});

/**
 * Наследование классов
 *
 * Для того, чтобы наследовать один "класс" от другого, требуется указать "класс"-предок первым аргументом в go.Class()
 */
go.Test("Inheritance", function() {

    var ParentClass = go.Class(null, {
    
        '__construct': (function(value) {
            this.value = value;
        }),
        
        'getValue': (function() {
            return this.value + this.getDelta();
        }),
        
        'getDelta': (function() {
            return 1;
        }),        
    
        'eoc': null
    });
    
    var ChildClass = go.Class(ParentClass, {
    
        'getDelta': (function() {
            return 2;
        }),
        
        'getOriginalValue': (function() {
            return this.value;
        }),
    
        'eoc': null
    });
    
    var obj = new ChildClass(5);
    
    ok(obj instanceof ChildClass, "obj instanceof Self");
    ok(obj instanceof ParentClass, "obj instanceof Parent");
    
    equal(typeof(obj.getValue), "function", "Child наследовал конструктор и getValue()");
    equal(obj.getValue(), 7, "Child переопределил getDelta()");
    equal(obj.getOriginalValue(), 5, "Child расширился getOriginalValue()");
});

/**
 * Доступ к "классу" из экземпляра, осуществляется через свойство "$self"
 */
go.Test("Self access", function() {

    /**
     * Тестовый класс
     *
     * @property int value
     *           связанное с объектом целочисленное значение
     */
    var TestClass = go.Class({
    
        '__construct': (function(value) {
            this.value = value;
        }),
        
        'getValue': (function() {
            return this.value;
        }),
        
        /**
         * Создать объект того же класса
         *
         * @param int delta
         *        на сколько сместить значение потомка, по сравнению со значением родителя
         * @return TestClass
         */
        'createChild': (function(delta) {
            return (new this.$self(this.value + delta));
        }),
    
        'eoc': null
    });
    
    var parent = new TestClass(5);
    var child  = parent.createChild(3);
    
    ok(child instanceof TestClass, "объект создал экземпляр своего же класса");
    equal(child.getValue(), 8);
});

/**
 * Статические методы (и свойства) - методы, принадлежащие не объектам, а самому классу (функции-конструктору).
 *
 * Методы, имеющие в названиии постфикс "_static" считаются статическими, они не попадают в объект, 
 * а добавляются к функции-конструктору (уже без постфикса).
 * this в контексте статических методов указывает на функцию-конструктор.
 */
go.Test("Static methods", function() {

    /**
     * Тестовый класс - Singleton
     * 
     * @property-static SingletonClass instance
     */
    var SingletonClass = go.Class({
    
        /**
         * Получить объект-синглтон
         *
         * @return SingletonClass
         */
        'getInstance_static': (function() {
            if (!this.instance) {
                this.instance = (new this());
            }
            return this.instance;
        }),
        
        'getName_static': (function() {
            return "Singleton";
        }),
        
        'getObjectName': (function() {
            return "obj = " + this.$self.getName();
        }),
        
        'eoc': null
    });

    equal(typeof(SingletonClass.getInstance), "function", "getInstance - статический метод");
    equal(typeof(SingletonClass.getInstance_static), "undefined", "постфикс static вырезан");
    equal(typeof(SingletonClass.getObjectname), "undefined", "getObjectName - остался обычным методом");
    
    var instance = SingletonClass.getInstance();
    ok(instance instanceof SingletonClass, "getInstance возвращает экземпляр данного класса");
    equal(SingletonClass.getInstance(), instance, "getInstance всегда возвращает один объект");
    
    equal(typeof(instance.getObjectName), "function", "getObjectName - обычный метод");
    equal(typeof(instance.getInstance_static), "undefined", "getInstance_static исчёз из объекта");
    equal(typeof(instance.getInstance), "undefined", "getInstance тоже не появился в объекте");
    
    equal(instance.getObjectName(), "obj = Singleton", "Вызов из обычного метода статического");
});

/**
 * Статические свойства также можно эмулировать через доступ к структурам, не перекрывая их.
 */
go.Test("Static property", function() {

    var TestClass = go.Class({
    
        /* Разделяемый всеми объектами список */
        'A': [],
        
        /* Перекрываемый в объектах список */
        'B': [],
    
        '__construct': (function() {
            this.B = [];
        }),
        
        'pushA': (function(value) {
            this.A.push(value);
        }),
        
        'popA': (function() {
            return this.A.pop();
        }),
        
        'pushB': (function(value) {
            this.B.push(value);
        }),
        
        'popB': (function() {
            return this.B.pop();
        }),
        
        'eoc': null
    });
    
    var one = new TestClass();
    var two = new TestClass();
    
    one.pushA(1);
    one.pushB(2);
    two.pushA(3);
    two.pushB(4);
    
    equal(one.popA(), 3, "A разделяется всеми объектами");
    equal(one.popB(), 2, "B свой у каждого");
});

/**
 * Статические методы также наследуются
 */
go.Test("Static inheritance", function() {

    var ParentClass = go.Class({
    
        'one_static': (function() {
            return "one " + this.two();
        }),
        
        'two_static': (function() {
            return "two";
        }),
    
        'eoc': null
    });
    
    var ChildClass = go.Class(ParentClass, {
    
        'two_static': (function() {
            return "notwo";
        }),
    
        'eoc': null
    });
    
    equal(ParentClass.one(), "one two");
    equal(ChildClass.one(), "one notwo");    
});

/**
 * Привязка методов к контексту объекта
 *
 * Методы с постфиксом "_bind" всегда вызываются в контексте объекта в котором созданы (постфикс при этом отрезается).
 * Это удобно, например, при назначении обработчиков событий.
 */
go.Test("Method bind", function() {

    /**
     * @property int x
     * @property int y
     */
    var TestClass = go.Class({
    
        '__construct': (function(x, y) {
            this.x = x;
            this.y = y;
        }),
        
        'getX': (function() {
            return this.x;
        }),
        
        'getY_bind': (function() {
            return this.y;
        }),
        
        'eoc': null    
    });
    
    var obj = new TestClass(1, 2);
    
    equal(typeof(obj.getY_bind), "undefined", "Метод с постфиксом _bind исчёз");
    equal(typeof(obj.getY), "function", "Зато появился getY без постфикса");
    
    var other = {
        'x': 3,
        'y': 4
    };
    
    other.getX = obj.getX;
    other.getY = obj.getY;
    
    equal(other.getX(), 3, "getX забыл о своём родном объекте");
    equal(other.getY(), 2, "getY помнит о своём родном объекте");
});

/**
 * Bind-методы также наследуются
 */
go.Test("Bind inheritance", function() {
    
    var ParentClass = go.Class({
    
        'setValue': (function(value) {
            this.value = value;
        }),
    
        'getValue_bind': (function() {
            return this.value;
        }),
        
        'eoc': null    
    });
    
    var ChildClass = go.Class(ParentClass, {
        
        '__construct': (function(value) {
            this.setValue(value);
        }),
        
        'eoc': null        
    });
    
    var obj = new ChildClass(7);
    var getValue = obj.getValue;
    
    equal(getValue(), 7, "bind-метод наследовался от родительского класса");    
});

/**
 * Абстрактные классы - просто для небольшого повышения структурированности
 *
 * Класс с установленным свойством "$abstract" считается асбтрактным и его нельзя инстанцировать.
 */
go.Test("Abstract", function() {

    /**
     * Абстрактный класс
     */
    var BaseClass = go.Class({
        '$abstract': true,
        
        'getX_static': (function() {
            return 'X';
        }),
        
        'eoc': null
    });
    
    /**
     * Наследник абстрактного класса, без переопределения $abstract - тоже абстрактный
     */
    var Base2Class = go.Class(BaseClass, {
    });
   
    /**
     * $abstract переопределён - нормальный класс
     */ 
    var FinalClass = go.Class(BaseClass, {
        '$abstract': false
    });

    raises(function() {
        var obj = new BaseClass();
    }, null, "Инстанцирование абстрактного класса вызывает исключение");
    
    raises(function() {
        var obj = new Base2Class();
    }, null, "Base2Class также абстрактный");    
    
    equal(BaseClass.getX(), "X", "Статические методы абстрактных классов нормально работают");

    var obj = new FinalClass(); 
    ok(obj instanceof BaseClass, "FinalClass нормально инстанцируется");
});

/**
 * Доступ к классу-родителю
 *
 * Предковый класс доступен через свойство "$parent", как объекта, так и самого "класса".
 * У корневых классов $parent=null
 */
go.Test("Parent class", function() {

    var ParentClass = go.Class({
    
        'getParentFromObject': (function() {
            return this.$parent;
        }),
        
        'getParentFromClass': (function() {
            return this.$self.$parent;
        }),
        
        'eoc': null
    });
    
    var ChildClass = go.Class(ParentClass, {
    });
    
    var parent = new ParentClass();
    var child  = new ChildClass();
 
    equal(child.getParentFromObject(), ParentClass, "this.$parent ссылается на класс-родитель");
    equal(child.getParentFromClass(), ParentClass, "Class.$parent ссылается на класс-родитель");
    
    equal(parent.getParentFromObject(), null, "Для корневого класса this.$parent=null");
    equal(parent.getParentFromClass(), null, "Для корневого класса Class.$parent=null");
});

/**
 * Каждый метод имеет два свойства, описывающие его положение в иерархии:
 * 1. "$self" - "класс" в котором он был описан
 * 2. "__parent" - ссылка на предыдущую реализацию данного метода
 *
 * Соответственно, вызов предыдущей реализации: arguments.callee.__parent(arg1, arg2);
 * Либо, если нужен контекст: arguments.callee.__parent.call(this, arg1, arg2);
 */
go.Test("Parent call", function() {

    var XClass = go.Class({
    
        '__construct': (function(x) {
            this.x = x;
        }),
        
        'getX': (function() {
            return this.x;
        }),
        
        'eoc': null
    });
    
    var YClass = go.Class(XClass, {
        
        '__construct': (function(x, y) {
            arguments.callee.__parent.call(this, x);
            this.y = y;
        }),
        
        'getY': (function() {            
            return this.y;            
        }),
        
        'getSelfMethod': (function() {
            return arguments.callee.$self;
        }),
        
        'eoc': null
    });

    var ZClass = go.Class(YClass, {
        
        '__construct': (function(x, y, z) {
            arguments.callee.__parent.call(this, x, y);
            this.z = z;
        }),
        
        'getZ': (function() {            
            return this.z;
        }),
        
        'eoc': null
    });

    var obj = new ZClass(1, 2, 3);
    
    equal(obj.getZ(), 3, "Z был установлен через конструктор");
    equal(obj.getY(), 2, "Y был установлен через предковый конструктор");
    equal(obj.getX(), 1, "X был установлен через цепочку доступа к предкам");
    
    equal(obj.getSelfMethod(), YClass, "method.$self возвращает класс в котором был определён метод");
    
    equal(ZClass.$parent.$parent, XClass, "Доступ по цепочке к изначальному предку");
});

/**
 * Свойства $self и __parent действуют аналогично и для static и bind-методов.
 */
go.Test("parent call (static, bind)", function() {

    var ParentClass = go.Class({
    
        'par_static': (function() {
            return "par_static";
        }),
        
        'self_static': (function() {
            return arguments.callee.$self;
        }),
        
        'par_bind': (function() {
            return "par_bind";
        }),
        
        'self_bind': (function() {
            return arguments.callee.$self;
        }),
    
        'eoc': null
    });    
    
    var ChildClass = go.Class(ParentClass, {
    
        'par_static': (function() {
            return "child " + arguments.callee.__parent();
        }),
        
        'par_bind': (function() {
            return "child " + arguments.callee.__parent();
        }),
    
        'eoc': null
    });
    
    equal(ChildClass.par(), "child par_static", "__parent для static-метода");
    equal(ChildClass.self(), ParentClass, "$self для static-метода");
    
    var obj = new ChildClass();
    equal(obj.par(), "child par_bind", "__parent для bind-метода");
    equal(obj.self(), ParentClass, "$self для bind-метода");
});

/**
 * "Разрушение" отжившего своё объекта производится через явный вызов метода destroy(), при этом происходит:
 * 1. Вызывается пользовательский деструктор "__destruct()"
 * 2. Производится разрушение todestroy-объектов (см. тест ниже)
 * 3. Все свойства и методы сбрасываются в null (за некоторыми исключениями, см. ниже)
 */
go.Test("Destroy", function() {

    var destroyed = 0;
    
    var TestClass = go.Class({
    
        '__construct': (function(value) {
            this.value = value;
        }),
        
        '__destruct': (function() {
            destroyed++;
        }),
        
        'getValue': (function() {
            return this.value;
        }),
        
        'eoc': null
    });
    
    var one = new TestClass(1);
    var two = new TestClass(2);
    
    equal(one.getValue(), 1);
    equal(one.value, 1);
    
    one.destroy();
    equal(destroyed, 1, "Был вызван __destruct()");
    
    equal(one.value, null, "Все свойства обнулены");
    equal(one.getValue, null, "Все методы обнулены");
    
    two.destroy();
    equal(destroyed, 2);
});

/**
 * После destroy() в null не обращаются только:
 * 1. Свойство "$destroyed" (true) - указывает, что объект уже разрушен.
 * 2. Метод "destroy()" - его можно вызвать повторно, но он ничего не будет делать.
 */
go.Test("One-time destroy", function() {

    var destroyed = 0;
    
    var TestClass = go.Class({
    
        '__construct': (function(value) {
            this.value = value;
        }),
        
        '__destruct': (function() {
            destroyed++;
        }),
        
        'eoc': null
    });
    
    var obj = new TestClass();
    
    ok(!obj.$destroyed, "$destroyed=false - ещё не разрушен");
    
    obj.destroy();
    equal(destroyed, 1);
    ok(obj.$destroyed, "$destroyed=true - объект разрушен");    
    
    obj.destroy();
    obj.destroy();
    equal(destroyed, 1, "destroy() можно продолжать вызывать, но он не имеет эффекта");
});

/**
 * Создаваемые внутри объекта другие объекты, можно добавлять в список $todestroy.
 * При разрушении родительского объекта, будут разрушены все объекты из данного списка.
 *
 * Можно воспользоваться методом:
 * __createObjectProperty(name, object) - создаёт свойство this[name]=object и заодно помещает объект в todestroy очередь.
 */
go.Test("Todestroy", function() {

    var TestClass = go.Class({
    
        /**
         * Создать A-объект (автоматически разрушаемый)
         */
        'createA': (function() {
            if (!this.a) {
                this.__createObjectProperty("a", new this.$self());
            }
            return this.a;
        }),
        
        /**
         * Создать B-объект (не разрушаемый)
         */
        'createB': (function() {
            if (!this.b) {
                this.b = new this.$self();
            }
            return this.b;
        }),
    
        'eoc': null
    });

    var obj = new TestClass();
    var objA = obj.createA();
    var objB = obj.createB();
    var objAA = objA.createA();
    var objAB = objA.createB();
    var objBA = objB.createA();
    var objBB = objB.createB();
    
    obj.destroy();
    ok(obj.$destroyed,    "obj уничтожен напрямую");
    ok(objA.$destroyed,   "objA уничтожен через obj");
    ok(!objB.$destroyed,  "objB не уничтожается");
    ok(objAA.$destroyed,  "objAA уничтожен через цепочку obj-objA");
    ok(!objAB.$destroyed, "objAB не должен уничтожаться");
    ok(!objBA.$destroyed, "objBA не уничтожен, так как не уничтожет родитель");
    ok(!objBB.$destroyed, "objBB не должен уничтожаться");

    objB.destroy();
    ok(objB.$destroyed,  "objB уничтожен напрямую");
    ok(objBA.$destroyed, "objBA уничтожен через objB");
    ok(!objBB.$destroyed, "objBB не должен уничтожаться");    
});
